/*
Copyright 2024 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package main

import (
	"bytes"
	"cmp"
	"fmt"
	"io"
	"reflect"
	"slices"
	"strconv"
	"strings"
	"unicode"

	"k8s.io/apimachinery/pkg/util/sets"
	"k8s.io/apimachinery/pkg/util/validation/field"
	"k8s.io/code-generator/cmd/validation-gen/util"
	"k8s.io/code-generator/cmd/validation-gen/validators"
	"k8s.io/gengo/v2/generator"
	"k8s.io/gengo/v2/namer"
	"k8s.io/gengo/v2/parser/tags"
	"k8s.io/gengo/v2/types"
	"k8s.io/klog/v2"
)

func mkPkgNames(pkg string, names ...string) []types.Name {
	result := make([]types.Name, 0, len(names))
	for _, name := range names {
		result = append(result, types.Name{Package: pkg, Name: name})
	}
	return result
}

var (
	fieldPkg            = "k8s.io/apimachinery/pkg/util/validation/field"
	fieldPkgSymbols     = mkPkgNames(fieldPkg, "ErrorList", "InternalError", "Path")
	fmtPkgSymbols       = mkPkgNames("fmt", "Errorf")
	safePkg             = "k8s.io/apimachinery/pkg/api/safe"
	safePkgSymbols      = mkPkgNames(safePkg, "Field", "Cast", "Value")
	operationPkg        = "k8s.io/apimachinery/pkg/api/operation"
	operationPkgSymbols = mkPkgNames(operationPkg, "Operation", "MatchesSubresource", "Update")
	contextPkg          = "context"
	contextPkgSymbols   = mkPkgNames(contextPkg, "Context")
	equalityPkg         = "k8s.io/apimachinery/pkg/api/equality"
	equalityPkgSymbols  = mkPkgNames(equalityPkg, "Semantic")
)

// genValidations produces a file with autogenerated validations.
type genValidations struct {
	generator.GoGenerator
	outputPackage  string
	inputToPkg     map[string]string // Maps input packages to generated validation packages
	rootTypes      []*types.Type
	discovered     *typeDiscoverer
	imports        namer.ImportTracker
	schemeRegistry types.Name
}

// NewGenValidations creates a new generator for the specified package.
func NewGenValidations(outputFilename, outputPackage string, rootTypes []*types.Type, discovered *typeDiscoverer, inputToPkg map[string]string, schemeRegistry types.Name) generator.Generator {
	return &genValidations{
		GoGenerator: generator.GoGenerator{
			OutputFilename: outputFilename,
		},
		outputPackage:  outputPackage,
		inputToPkg:     inputToPkg,
		rootTypes:      rootTypes,
		discovered:     discovered,
		imports:        generator.NewImportTrackerForPackage(outputPackage),
		schemeRegistry: schemeRegistry,
	}
}

func (g *genValidations) Namers(_ *generator.Context) namer.NameSystems {
	// Have the raw namer for this file track what it imports.
	return namer.NameSystems{
		"raw": namer.NewRawNamer(g.outputPackage, g.imports),
	}
}

func (g *genValidations) Filter(_ *generator.Context, t *types.Type) bool {
	// We want to emit code for all root types.
	if slices.Contains(g.rootTypes, t) {
		return true
	}
	// We want to emit for any other type that is transitively part of a root
	// type and has validations.
	n := g.discovered.typeNodes[t]
	return n != nil && g.hasValidations(n)
}

func (g *genValidations) Imports(_ *generator.Context) (imports []string) {
	var importLines []string
	for _, singleImport := range g.imports.ImportLines() {
		if g.isOtherPackage(singleImport) {
			importLines = append(importLines, singleImport)
		}
	}
	return importLines
}

func (g *genValidations) isOtherPackage(pkg string) bool {
	if pkg == g.outputPackage {
		return false
	}
	if strings.HasSuffix(pkg, `"`+g.outputPackage+`"`) {
		return false
	}
	return true
}

func (g *genValidations) Init(c *generator.Context, w io.Writer) error {
	klog.V(5).Infof("emitting registration code")
	sw := generator.NewSnippetWriter(w, c, "$", "$")
	g.emitRegisterFunction(c, g.schemeRegistry, sw)
	if err := sw.Error(); err != nil {
		return err
	}
	return nil
}

func (g *genValidations) GenerateType(c *generator.Context, t *types.Type, w io.Writer) error {
	klog.V(5).Infof("emitting validation code for type %v", t)

	sw := generator.NewSnippetWriter(w, c, "$", "$")
	g.emitValidationVariables(c, t, sw)
	g.emitValidationFunction(c, t, sw)
	if err := sw.Error(); err != nil {
		return err
	}
	return nil
}

// typeDiscoverer contains fields necessary to build graphs of types.
type typeDiscoverer struct {
	initialized bool
	validator   validators.Validator
	inputToPkg  map[string]string

	// constantsByType holds a map of type to constants of that type.
	constantsByType map[*types.Type][]*validators.Constant

	// typeNodes holds a map of gengo Type to typeNode for all of the types
	// encountered during discovery.
	typeNodes map[*types.Type]*typeNode
}

// NewTypeDiscoverer creates a NewTypeDiscoverer.
// Init must be called before calling DiscoverType.
func NewTypeDiscoverer(validator validators.Validator, inputToPkg map[string]string) *typeDiscoverer {
	return &typeDiscoverer{
		validator:       validator,
		inputToPkg:      inputToPkg,
		constantsByType: map[*types.Type][]*validators.Constant{},
		typeNodes:       map[*types.Type]*typeNode{},
	}
}

// Init uses the generator context to prepare for type discovery.
func (td *typeDiscoverer) Init(c *generator.Context) error {
	packages := c.Universe
	for _, pkg := range packages {
		// We only care about packages we are generating for or are readonly.
		if _, ok := td.inputToPkg[pkg.Path]; !ok {
			continue
		}
		for _, cnst := range pkg.Constants {
			context := validators.Context{
				Scope:      validators.ScopeConst,
				Type:       cnst.Underlying,
				Path:       nil, // NA when discovering a constant
				Member:     nil, // NA when discovering a constant
				ParentPath: nil, // NA when discovering a constant
			}
			tgs, err := td.validator.ExtractTags(context, cnst.CommentLines)
			if err != nil {
				return fmt.Errorf("constant %s: %w", cnst.Name, err)
			}
			if len(tgs) > 0 {
				// Also check that the tgs are valid.
				if _, err := td.validator.ExtractValidations(context, tgs...); err != nil {
					return fmt.Errorf("constant %s: %w", cnst.Name, err)
				}
			}
			td.constantsByType[cnst.Underlying] = append(td.constantsByType[cnst.Underlying], &validators.Constant{Constant: cnst, Tags: tgs})
		}
	}
	td.initialized = true
	return nil
}

// childNode represents a type which is used in another type (e.g. a struct
// field).
type childNode struct {
	name      string      // the field name in the parent, populated when this node is a struct field
	jsonName  string      // always populated when name is populated
	childType *types.Type // the real type of the child (may be a pointer)
	node      *typeNode   // the node of the child's value type, or nil if it is in a foreign package

	fieldValidations validators.Validations // validations on the field

	// These are not the same as fieldValidations, and are not considered in
	// hasValidations. These let us emit the iteration code for list and
	// map types, but we might not have enough information to know if we can
	// skip them at discovery time.
	fieldValIterations validators.Validations // validations on each val
	fieldKeyIterations validators.Validations // validations on each key
}

// typeNode represents a node in the type-graph, annotated with information
// about validations.  Everything in this type, transitively, is assoctiated
// with the type, and not any specific instance of that type (e.g. when used as
// a field in a struct.
type typeNode struct {
	valueType *types.Type // never a pointer, but may be a map, slice, struct, etc.
	funcName  types.Name  // populated when this type is has a validation function

	fields     []*childNode // populated when this type is a struct
	key        *childNode   // populated when this type is a map
	elem       *childNode   // populated when this type is a map or slice
	underlying *childNode   // populated when this type is an alias

	typeValidations validators.Validations // validations on the type

	// These are not the same as typeValidations, and are not considered in
	// hasValidations. These let us emit the iteration code for list and
	// map types, but we might not have enough information to know if we can
	// skip them at discovery time.
	typeValIterations validators.Validations // validations on each val
	typeKeyIterations validators.Validations // validations on each key
}

// DiscoverType walks the given type recursively, building a type-graph in this
// typeDiscoverer.  If this is called multiple times for different types, the
// graphs will be will be merged.
func (td *typeDiscoverer) DiscoverType(t *types.Type) error {
	if !td.initialized {
		return fmt.Errorf("typeDiscoverer not initialized")
	}
	if t.Kind == types.Pointer {
		return fmt.Errorf("type %v: pointer root-types are not supported", t)
	}
	fldPath := field.NewPath(t.Name.String())
	if node, err := td.discoverType(t, fldPath); err != nil {
		return err
	} else if node == nil {
		panic(fmt.Sprintf("discovered a nil node for type %v", t))
	}
	return nil
}

// discoverType walks the given type recursively and returns a typeNode
// representing it. This does not distinguish between discovering a type
// definition and discovering a field of a struct.  The first time it
// encounters a type it has not seen before, it will explore that type. If it
// finds a type it has already processed, it will return the existing node.
func (td *typeDiscoverer) discoverType(t *types.Type, fldPath *field.Path) (*typeNode, error) {
	// With the exception of builtins (which gengo puts in package ""), we
	// can't traverse into packages which are not being processed by this tool.
	if t.Name.Package != "" {
		_, ok := td.inputToPkg[t.Name.Package]
		if !ok {
			return nil, nil
		}
	}

	// Catch some cases that we don't want to handle (yet?).  This happens
	// as early as possible to make all the other code simpler.
	if err := td.verifySupportedType(t); err != nil {
		return nil, fmt.Errorf("field %s (%s): %w", fldPath.String(), t, err)
	}

	// Discovery applies to values, not pointers.
	if t.Kind == types.Pointer {
		return td.discoverType(t.Elem, fldPath)
	}

	// If we have done this type already, we can stop here and break any
	// recursion.
	if node, found := td.typeNodes[t]; found {
		return node, nil
	}
	klog.V(4).InfoS("discoverType", "type", t, "kind", t.Kind, "path", fldPath.String())

	// This is the type-node being assembled in the rest of this function.
	thisNode := &typeNode{
		valueType: t,
	}
	td.typeNodes[t] = thisNode

	// If we are descending into a named type...
	switch t.Kind {
	case types.Alias, types.Struct:
		// Reboot the field path for better logging.  Otherwise the field path
		// might come in as something like <type1>.<field1>.<field2> which is
		// true, but not super useful.
		fldPath = field.NewPath(t.Name.String())

		// Find its validation function for later use.
		if fn, ok := td.getValidationFunctionName(t); ok {
			thisNode.funcName = fn
		}
	}

	// Discover into this type before extracting type validations.
	switch t.Kind {
	case types.Builtin, types.Interface:
		// Nothing more to do.
	case types.Alias:
		// Discover the underlying type.
		//
		// Note: By the language definition, what gengo calls "Aliases" (really
		// just "type definitions") have underlying types of the type literal.
		// In other words, if we define `type T1 string` and `type T2 T1`, the
		// underlying type of T2 is string, not T1.  This means that:
		//    1) We will emit code for both underlying types. If the underlying
		//       type is a struct with many fields, we will emit two identical
		//       functions.
		//    2) Validating a field of type T2 will NOT call any validation
		//       defined on the type T1.
		//    3) In the case of a type definition whose RHS is a struct which
		//       has fields with validation tags, the validation for those fields
		//       WILL be called from the generated for for the new type.
		if node, err := td.discoverType(t.Underlying, fldPath); err != nil {
			return nil, err
		} else {
			thisNode.underlying = &childNode{
				childType: t.Underlying,
				node:      node,
			}
		}
	case types.Struct:
		// Discover into this struct, recursively.
		if err := td.discoverStruct(thisNode, fldPath); err != nil {
			return nil, err
		}
	case types.Slice:
		// Discover the element type.
		if node, err := td.discoverType(t.Elem, fldPath.Key("vals")); err != nil {
			return nil, err
		} else {
			thisNode.elem = &childNode{
				childType: t.Elem,
				node:      node,
			}
		}
	case types.Map:
		// Discover the key type.
		if node, err := td.discoverType(t.Key, fldPath.Key("keys")); err != nil {
			return nil, err
		} else {
			thisNode.key = &childNode{
				childType: t.Key,
				node:      node,
			}
		}

		// Discover the element type.
		if node, err := td.discoverType(t.Elem, fldPath.Key("vals")); err != nil {
			return nil, err
		} else {
			thisNode.elem = &childNode{
				childType: t.Elem,
				node:      node,
			}
		}
	}

	// Extract any type-attached validation rules.  We do this AFTER descending
	// into the type, so that these validators have access to the full type.
	// For example, all struct field validators get called before the type
	// validators.  This does not influence the order in which the validations
	// are called in emitted code, just how we evaluate what to emit.
	switch t.Kind {
	case types.Alias, types.Struct:
		if fldPath.String() != t.String() {
			panic(fmt.Sprintf("path for type != the type name: %s, %s", t.String(), fldPath.String()))
		}
		consts := td.constantsByType[t]
		context := validators.Context{
			Scope:      validators.ScopeType,
			Type:       t,
			Path:       fldPath,
			Member:     nil, // NA when discovering a type
			ParentPath: nil, // NA when discovering a type
			Constants:  consts,
		}
		extractedTags, err := td.validator.ExtractTags(context, t.CommentLines)
		if err != nil {
			return nil, fmt.Errorf("%v: %w", fldPath, err)
		}
		if validations, err := td.validator.ExtractValidations(context, extractedTags...); err != nil {
			return nil, fmt.Errorf("%v: %w", fldPath, err)
		} else if validations.Empty() {
			klog.V(6).InfoS("no type-attached validations", "type", t)
		} else {
			if util.NonPointer(util.NativeType(t)).Kind == types.Map && util.NonPointer(util.NativeType(t)).Elem.Kind == types.Slice {
				return nil, fmt.Errorf("field %s: validation for map of slices is not supported", fldPath)
			}
			klog.V(5).InfoS("found type-attached validations", "n", validations.Len(), "type", t)
			thisNode.typeValidations.Add(validations)
		}

		// Handle type definitions whose output depends on the rest of type
		// discovery being complete. In particular, aliases to lists and maps need
		// iteration, but we don't want to iterate them if the key or value types
		// don't actually have validations. We also want to handle non-included
		// types and make users tell us what they intended. Lastly, we want to
		// handle recursive types, but we need to finish discovering the type
		// before we know if there are other validations, again so we don't emit
		// empty functions.
		if t.Kind == types.Alias {
			underlying := thisNode.underlying

			switch t.Underlying.Kind {
			case types.Slice:
				// Validate each value.
				if elemNode := underlying.node.elem.node; elemNode == nil {
					if !thisNode.typeValidations.OpaqueValType {
						return nil, fmt.Errorf("%v: value type %v is in a non-included package; "+
							"either add this package to validation-gen's --readonly-pkg flag, "+
							"or add +k8s:eachVal=+k8s:opaqueType to the field to skip validation",
							fldPath, underlying.node.elem.childType)
					}
				} else if thisNode.typeValidations.OpaqueValType {
					// If the type is marked as opaque, we can treat it as it is
					// were in a non-included package.
				} else {
					// If the value type is a named type, call the validation
					// function for each element.
					if funcName := elemNode.funcName; funcName.Name != "" {
						// Save the iteration validation while we have all the
						// information we need. Later we can check if we
						// actually need it.
						//
						// Note: the first argument to Function() is really
						// only for debugging.
						v, err := validators.ForEachVal(fldPath, thisNode.valueType,
							validators.Function("iterateListValues", validators.DefaultFlags, funcName).
								WithComment("iterate the list and call the type's validation function"))
						if err != nil {
							return nil, fmt.Errorf("generating list iteration: %w", err)
						} else {
							thisNode.typeValIterations.Add(v)
						}
					}
				}
			case types.Map:
				// Validate each key.
				if keyNode := underlying.node.key.node; keyNode == nil {
					if !thisNode.typeValidations.OpaqueKeyType {
						return nil, fmt.Errorf("%v: key type %v is in a non-included package; "+
							"either add this package to validation-gen's --readonly-pkg flag, "+
							"or add +k8s:eachKey=+k8s:opaqueType to the field to skip validation",
							fldPath, underlying.node.elem.childType)
					}
				} else if thisNode.typeValidations.OpaqueKeyType {
					// If the type is marked as opaque, we can treat it as it is
					// were in a non-included package.
				} else {
					// If the key type is a named type, call the validation
					// function for each key.
					if funcName := keyNode.funcName; funcName.Name != "" {
						// Save the iteration validation while we have all the
						// information we need. Later we can check if we
						// actually need it.
						//
						// Note: the first argument to Function() is really
						// only for debugging.
						v, err := validators.ForEachKey(fldPath, underlying.childType,
							validators.Function("iterateMapKeys", validators.DefaultFlags, funcName).
								WithComment("iterate the map and call the key type's validation function"))
						if err != nil {
							return nil, fmt.Errorf("generating map key iteration: %w", err)
						} else {
							thisNode.typeKeyIterations.Add(v)
						}
					}
				}
				// Validate each value.
				if elemNode := underlying.node.elem.node; elemNode == nil {
					if !thisNode.typeValidations.OpaqueValType {
						return nil, fmt.Errorf("%v: value type %v is in a non-included package; "+
							"either add this package to validation-gen's --readonly-pkg flag, "+
							"or add +k8s:eachVal=+k8s:opaqueType to the field to skip validation",
							fldPath, underlying.node.elem.childType)
					}
				} else if thisNode.typeValidations.OpaqueValType {
					// If the type is marked as opaque, we can treat it as it is
					// were in a non-included package.
				} else {
					// If the value type is a named type, call the validation
					// function for each element.
					if funcName := elemNode.funcName; funcName.Name != "" {
						// Save the iteration validation while we have all the
						// information we need. Later we can check if we
						// actually need it.
						//
						// Note: the first argument to Function() is really
						// only for debugging.
						v, err := validators.ForEachVal(fldPath, underlying.childType,
							validators.Function("iterateMapValues", validators.DefaultFlags, funcName).
								WithComment("iterate the map and call the value type's validation function"))
						if err != nil {
							return nil, fmt.Errorf("generating map value iteration: %w", err)
						} else {
							thisNode.typeValIterations.Add(v)
						}
					}
				}
			}
		}
	}

	return thisNode, nil
}

// verifySupportedType checks whether the given type is supported.
func (td *typeDiscoverer) verifySupportedType(t *types.Type) error {
	switch t.Kind {
	case types.Builtin, types.Struct:
		// Allowed
	case types.Interface:
		// We can't do much with interfaces, but they pop up in some places
		// like RawExtension.
	case types.Alias:
		if t.Underlying.Kind == types.Pointer {
			return fmt.Errorf("typedefs to pointers are not supported")
		}
	case types.Pointer:
		pointee := util.NativeType(t.Elem)
		switch pointee.Kind {
		case types.Pointer:
			return fmt.Errorf("pointers to pointers are not supported")
		case types.Slice, types.Array:
			return fmt.Errorf("pointers to lists are not supported")
		case types.Map:
			return fmt.Errorf("pointers to maps are not supported")
		}
	case types.Array:
		return fmt.Errorf("fixed-size arrays are not supported")
	case types.Slice:
		elem := util.NativeType(t.Elem)
		switch elem.Kind {
		case types.Pointer:
			return fmt.Errorf("lists of pointers are not supported")
		case types.Slice:
			if util.NativeType(elem.Elem) != types.Byte {
				return fmt.Errorf("lists of lists are not supported")
			}
		case types.Map:
			return fmt.Errorf("lists of maps are not supported")
		}
	case types.Map:
		key := util.NativeType(t.Key)
		if key != types.String {
			return fmt.Errorf("maps with non-string keys are not supported")
		}
		elem := util.NativeType(t.Elem)
		switch elem.Kind {
		case types.Pointer:
			return fmt.Errorf("maps of pointers are not supported")
		case types.Map:
			return fmt.Errorf("maps of maps are not supported")
		}
	default:
		return fmt.Errorf("kind %v is not supported", t.Kind)
	}

	return nil
}

// discoverStruct walks a struct type recursively.
func (td *typeDiscoverer) discoverStruct(thisNode *typeNode, fldPath *field.Path) error {
	var fields []*childNode

	klog.V(5).InfoS("discoverStruct", "type", thisNode.valueType)

	// Discover into each field of this struct.
	for _, memb := range thisNode.valueType.Members {
		name := memb.Name

		// Only do exported fields.
		if unicode.IsLower([]rune(name)[0]) {
			continue
		}

		// If we try to emit code for this field and find no JSON name, we
		// will abort.
		jsonName := ""
		if commentTags, ok := tags.LookupJSON(memb); ok {
			jsonName = commentTags.Name
		}

		var childPath *field.Path
		if jsonName != "" {
			childPath = fldPath.Child(jsonName)
		} else {
			childPath = fldPath.Child(name)
		}

		// Discover the field type.
		klog.V(5).InfoS("field", "name", name, "jsonName", jsonName, "type", memb.Type, "path", childPath)
		childType := memb.Type
		var child *childNode
		if node, err := td.discoverType(childType, childPath); err != nil {
			return err
		} else {
			child = &childNode{
				name:      name,
				jsonName:  jsonName,
				childType: childType,
				node:      node,
			}
		}

		// Extract any field-attached validation rules.
		context := validators.Context{
			Scope:      validators.ScopeField,
			Type:       childType,
			Path:       childPath,
			Member:     &memb,
			ParentPath: fldPath,
		}

		tags, err := td.validator.ExtractTags(context, memb.CommentLines)
		if err != nil {
			return fmt.Errorf("field %s: %w", childPath.String(), err)
		}
		if validations, err := td.validator.ExtractValidations(context, tags...); err != nil {
			return fmt.Errorf("field %s: %w", childPath.String(), err)
		} else if validations.Empty() {
			klog.V(6).InfoS("no field-attached validations", "field", childPath)
		} else {
			klog.V(5).InfoS("found field-attached validations", "n", validations.Len(), "field", childPath)
			if util.NonPointer(util.NativeType(childType)).Kind == types.Map && util.NonPointer(util.NativeType(childType)).Elem.Kind == types.Slice {
				return fmt.Errorf("field %s: validation for map of slices is not supported", childPath)
			}
			child.fieldValidations.Add(validations)
			// TODO: re-visit erroring on specific cases where variable generation is not supported for field validations
			// currently there are some cases where we want variable generation for field validations
		}

		// Handle non-included types.
		switch util.NonPointer(childType).Kind {
		case types.Struct, types.Alias:
			if child.node == nil { // a non-included type
				if !child.fieldValidations.OpaqueType {
					return fmt.Errorf("%v: type %v is in a non-included package; "+
						"either add this package to validation-gen's --readonly-pkg flag, "+
						"or add +k8s:opaqueType to the field to skip validation",
						childPath, childType.String())
				}
			} else if child.fieldValidations.OpaqueType {
				// If the field is marked as opaque, we can treat it as it is
				// were in a non-included package.
				child.node = nil
			}
		}

		// Add any other field-attached "special" validators. We need to do
		// this after all the other field validation has been processed,
		// because some of this is conditional on whether other validations
		// were emitted (to avoid emitting empty functions).
		//
		// We do this here, rather than in discoverType() because we need to
		// know information about the field, not just the type.
		switch childType.Kind {
		case types.Slice:
			// Validate each value of a list field.
			if elemNode := child.node.elem.node; elemNode == nil {
				if !child.fieldValidations.OpaqueValType {
					return fmt.Errorf("%v: value type %v is in a non-included package; "+
						"either add this package to validation-gen's --readonly-pkg flag, "+
						"or add +k8s:eachVal=+k8s:opaqueType to the field to skip validation",
						childPath, childType.Elem.String())
				}
			} else if child.fieldValidations.OpaqueValType {
				// If the field is marked as opaque, we can treat it as it is
				// were in a non-included package.
			} else {
				// If the list's value type is a named type, call the validation
				// function for each element.
				if funcName := elemNode.funcName; funcName.Name != "" {
					// Save the iteration validation while we have all the
					// information we need. Later we can check if we
					// actually need it.
					//
					// Note: the first argument to Function() is really
					// only for debugging.
					v, err := validators.ForEachVal(childPath, childType,
						validators.Function("iterateListValues", validators.DefaultFlags, funcName).
							WithComment("iterate the list and call the type's validation function"))
					if err != nil {
						return fmt.Errorf("generating list iteration: %w", err)
					} else {
						child.fieldValIterations.Add(v)
					}
				}
			}
		case types.Map:
			// Validate each key of a map field.
			if keyNode := child.node.key.node; keyNode == nil {
				if !child.fieldValidations.OpaqueKeyType {
					return fmt.Errorf("%v: key type %v is in a non-included package; "+
						"either add this package to validation-gen's --readonly-pkg flag, "+
						"or add +k8s:eachKey=+k8s:opaqueType to the field to skip validation",
						childPath, childType.Key.String())
				}
			} else if child.fieldValidations.OpaqueKeyType {
				// If the field is marked as opaque, we can treat it as it is
				// were in a non-included package.
			} else {
				// If the map's key type is a named type, call the validation
				// function for each key.
				if funcName := keyNode.funcName; funcName.Name != "" {
					// Save the iteration validation while we have all the
					// information we need. Later we can check if we
					// actually need it.
					//
					// Note: the first argument to Function() is really
					// only for debugging.
					v, err := validators.ForEachKey(childPath, childType,
						validators.Function("iterateMapKeys", validators.DefaultFlags, funcName).
							WithComment("iterate the map and call the key type's validation function"))
					if err != nil {
						return fmt.Errorf("generating map key iteration: %w", err)
					} else {
						child.fieldKeyIterations.Add(v)
					}
				}
			}
			// Validate each value of a map field.
			if elemNode := child.node.elem.node; elemNode == nil {
				if !child.fieldValidations.OpaqueValType {
					return fmt.Errorf("%v: value type %v is in a non-included package; "+
						"either add this package to validation-gen's --readonly-pkg flag, "+
						"or add +k8s:eachVal=+k8s:opaqueType to the field to skip validation",
						childPath, childType.Elem.String())
				}
			} else if child.fieldValidations.OpaqueValType {
				// If the field is marked as opaque, we can treat it as it is
				// were in a non-included package.
			} else {
				// If the map's value type is a named type, call the validation
				// function for each element.
				if funcName := elemNode.funcName; funcName.Name != "" {
					// Save the iteration validation while we have all the
					// information we need. Later we can check if we
					// actually need it.
					//
					// Note: the first argument to Function() is really
					// only for debugging.
					v, err := validators.ForEachVal(childPath, childType,
						validators.Function("iterateMapValues", validators.DefaultFlags, funcName).
							WithComment("iterate the map and call the value type's validation function"))
					if err != nil {
						return fmt.Errorf("generating map value iteration: %w", err)
					} else {
						child.fieldValIterations.Add(v)
					}
				}
			}
		}

		fields = append(fields, child)
	}

	thisNode.fields = fields
	return nil
}

// getValidationFunctionName looks up the name of the specified type's
// validation function.
//
// TODO: Currently this is a "blind" call - we hope that the expected function
// exists, but we don't verify that, and we only emit calls into packages which
// are being processed by this generator. For cross-package calls we will need
// to verify the target, either by naming convention + fingerprint or by
// explicit comment-tags or something.
func (td *typeDiscoverer) getValidationFunctionName(t *types.Type) (types.Name, bool) {
	pkg, ok := td.inputToPkg[t.Name.Package]
	if !ok {
		return types.Name{}, false
	}
	return types.Name{Package: pkg, Name: "Validate_" + t.Name.Name}, true
}

func mkSymbolArgs(c *generator.Context, names []types.Name) generator.Args {
	args := generator.Args{}
	for _, name := range names {
		args[name.Name] = c.Universe.Type(name)
	}
	return args
}

// hasValidations checks whether the given typeNode has any
// validations, transitively.
func (g *genValidations) hasValidations(n *typeNode) bool {
	seen := map[*typeNode]bool{}
	return g.hasValidationsImpl(n, seen)
}

// hasValidationsImpl implements hasValidations without risk of infinite
// recursion.
func (g *genValidations) hasValidationsImpl(n *typeNode, seen map[*typeNode]bool) bool {
	if n == nil {
		return false
	}

	if seen[n] {
		return false
	}
	seen[n] = true

	if !n.typeValidations.Empty() {
		return true
	}
	allChildren := n.fields
	if n.key != nil {
		allChildren = append(allChildren, n.key)
	}
	if n.elem != nil {
		allChildren = append(allChildren, n.elem)
	}
	if n.underlying != nil {
		allChildren = append(allChildren, n.underlying)
	}
	for _, c := range allChildren {
		if !c.fieldValidations.Empty() {
			return true
		}
		if g.hasValidationsImpl(c.node, seen) {
			return true
		}
	}
	return false
}

// emitRegisterFunction emits the type-registration logic for validation
// functions.
func (g *genValidations) emitRegisterFunction(c *generator.Context, schemeRegistry types.Name, sw *generator.SnippetWriter) {
	scheme := c.Universe.Type(schemeRegistry)
	schemePtr := &types.Type{
		Kind: types.Pointer,
		Elem: scheme,
	}

	sw.Do("func init() { localSchemeBuilder.Register(RegisterValidations)}\n\n", nil)

	sw.Do("// RegisterValidations adds validation functions to the given scheme.\n", nil)
	sw.Do("// Public to allow building arbitrary schemes.\n", nil)
	sw.Do("func RegisterValidations(scheme $.|raw$) error {\n", schemePtr)
	for _, rootType := range g.rootTypes {
		if !g.hasValidations(g.discovered.typeNodes[rootType]) {
			continue
		}

		node := g.discovered.typeNodes[rootType]
		if node == nil {
			panic(fmt.Sprintf("found nil node for root-type %v", rootType))
		}

		targs := generator.Args{
			"rootType":  rootType,
			"typePfx":   "",
			"field":     mkSymbolArgs(c, fieldPkgSymbols),
			"fmt":       mkSymbolArgs(c, fmtPkgSymbols),
			"operation": mkSymbolArgs(c, operationPkgSymbols),
			"safe":      mkSymbolArgs(c, safePkgSymbols),
			"context":   mkSymbolArgs(c, contextPkgSymbols),
		}
		if !util.IsNilableType(rootType) {
			targs["typePfx"] = "*"
		}

		// This uses a typed nil pointer, rather than a real instance because
		// we need the type information, but not an instance of the type.
		sw.Do("// type $.rootType|name$\n", targs)
		sw.Do("scheme.AddValidationFunc(", targs)
		sw.Do("    ($.typePfx$$.rootType|raw$)(nil), ", targs)
		sw.Do("    func(ctx $.context.Context$, op $.operation.Operation|raw$, obj, oldObj interface{}) $.field.ErrorList|raw$ {\n", targs)

		sw.Do("switch op.Request.SubresourcePath() {\n", nil)
		sw.Do("case ", nil)
		for i, s := range g.toResourceList(rootType) {
			if i > 0 {
				sw.Do(", ", nil)
			}
			sw.Do("$.$", s)
		}
		sw.Do(":\n", nil)
		sw.Do("    return $.rootType|objectvalidationfn$(", targs)
		sw.Do("               ctx, ", targs)
		sw.Do("               op, ", targs)
		sw.Do("               nil /* fldPath */, ", targs)
		sw.Do("               obj.($.typePfx$$.rootType|raw$), ", targs)
		sw.Do("               $.safe.Cast|raw$[$.typePfx$$.rootType|raw$](oldObj))\n", targs)
		sw.Do("  }\n", targs)
		sw.Do("  return $.field.ErrorList|raw${", targs)
		sw.Do("      $.field.InternalError|raw$(", targs)
		sw.Do("          nil, ", targs)
		sw.Do("          $.fmt.Errorf|raw$(\"no validation found for %T, subresource: %v\", obj, op.Request.SubresourcePath()))", targs)
		sw.Do("  }\n", targs)
		sw.Do("})\n", targs)
	}
	sw.Do("return nil\n", nil)
	sw.Do("}\n\n", nil)
}

// toResourceList returns a list of resources that are supported by a kind.
func (g *genValidations) toResourceList(rootType *types.Type) []string {
	supportedSubresources := supportedSubresourceTags(rootType)

	if subresource, isSubresource := isSubresourceTag(rootType); isSubresource {
		supportedSubresources.Insert(subresource)
	} else {
		supportedSubresources.Insert("/")
	}
	supported := supportedSubresources.UnsortedList()
	slices.Sort(supported)
	for i, subresource := range supported {
		supported[i] = strconv.Quote(subresource)
	}
	return supported
}

// emitValidationFunction emits a validation function for the specified type.
func (g *genValidations) emitValidationFunction(c *generator.Context, t *types.Type, sw *generator.SnippetWriter) {
	if !g.hasValidations(g.discovered.typeNodes[t]) {
		return
	}

	targs := generator.Args{
		"inType":     t,
		"field":      mkSymbolArgs(c, fieldPkgSymbols),
		"operation":  mkSymbolArgs(c, operationPkgSymbols),
		"context":    mkSymbolArgs(c, contextPkgSymbols),
		"objTypePfx": "*",
	}
	if util.IsNilableType(t) {
		targs["objTypePfx"] = ""
	}

	node := g.discovered.typeNodes[t]
	if node == nil {
		panic(fmt.Sprintf("found nil node for root-type %v", t))
	}
	sw.Do("// $.inType|objectvalidationfn$ validates an instance of $.inType|name$ according\n", targs)
	sw.Do("// to declarative validation rules in the API schema.\n", targs)
	sw.Do("func $.inType|objectvalidationfn$(", targs)
	sw.Do("    ctx $.context.Context|raw$, ", targs)
	sw.Do("    op $.operation.Operation|raw$, ", targs)
	sw.Do("    fldPath *$.field.Path|raw$, ", targs)
	sw.Do("    obj, oldObj $.objTypePfx$$.inType|raw$) ", targs)
	sw.Do("(errs $.field.ErrorList|raw$) {\n", targs)
	fakeChild := &childNode{
		node:      node,
		childType: t,
	}
	g.emitValidationForChild(c, fakeChild, sw)
	sw.Do("return errs\n", nil)
	sw.Do("}\n\n", nil)
}

// emitValidationForChild emits code for the specified childNode, calling
// type-attached validations and then descending into the type (e.g. struct
// fields).
//
// Emitted code assumes that the value in question is always a pair of nilable
// variables named "obj" and "oldObj", and the field path to this value is
// named "fldPath".
//
// This function assumes that thisChild.node is not nil.
func (g *genValidations) emitValidationForChild(c *generator.Context, thisChild *childNode, sw *generator.SnippetWriter) {
	thisNode := thisChild.node
	inType := thisNode.valueType

	targs := generator.Args{
		"inType": inType,
		"field":  mkSymbolArgs(c, fieldPkgSymbols),
		"safe":   mkSymbolArgs(c, safePkgSymbols),
	}

	didSome := false // for prettier output later

	// Emit code for type-attached validations.
	if validations := thisNode.typeValidations; !validations.Empty() {
		switch thisNode.valueType.Kind {
		case types.Struct, types.Alias: // OK
		default:
			panic(fmt.Sprintf("unexpected type-validations on type %v, kind %s", thisNode.valueType, thisNode.valueType.Kind))
		}
		emitComments(validations.Comments, sw)
		emitCallsToValidators(c, validations.Functions, sw)
		if thisNode.valueType.Kind == types.Alias {
			underlyingNode := thisNode.underlying.node
			switch underlyingNode.valueType.Kind {
			case types.Slice:
				// If this field is a list and the value-type has validations,
				// call its validation function.
				if validations := thisNode.typeValIterations; g.hasValidations(underlyingNode.elem.node) && !validations.Empty() {
					emitComments(validations.Comments, sw)
					emitCallsToValidators(c, validations.Functions, sw)
				}
			case types.Map:
				// If this field is a map and the key-type has validations,
				// call its validation function.
				if validations := thisNode.typeKeyIterations; g.hasValidations(underlyingNode.key.node) && !validations.Empty() {
					emitComments(validations.Comments, sw)
					emitCallsToValidators(c, validations.Functions, sw)
				}
				// If this field is a map and the value-type has validations,
				// call its validation function.
				if validations := thisNode.typeValIterations; g.hasValidations(underlyingNode.elem.node) && !validations.Empty() {
					emitComments(validations.Comments, sw)
					emitCallsToValidators(c, validations.Functions, sw)
				}
			}
		}
		sw.Do("\n", nil)
		didSome = true
	}

	// Descend into the type.
	switch inType.Kind {
	case types.Builtin:
		// Nothing further.
	case types.Slice:
		// Nothing further
	case types.Map:
		// Nothing further
	case types.Alias:
		g.emitValidationForChild(c, thisNode.underlying, sw)
	case types.Struct:
		for _, fld := range thisNode.fields {
			if len(fld.name) == 0 {
				panic(fmt.Sprintf("missing field name in type %s (field-type %s)", thisNode.valueType, fld.childType))
			}
			// Missing JSON name is checked iff we have code to emit.

			// Accumulate into a buffer so we don't emit empty functions.
			buf := bytes.NewBuffer(nil)
			bufsw := sw.Dup(buf)

			// On ratcheting checks:
			//
			// We emit ratcheting checks ONLY for struct fields and ONLY when
			// that field has some validations to call.
			//
			// We DO NOT emit ratchet checks inside type-specific validation
			// functions, because that leads to repeated ratchet checking which
			// is almost never useful work (keep reading).
			//
			// The consequence of this is that a caller of a type's validation
			// function is assumed to have already done a ratchet check (which
			// is true for all generated code (except root types, keep
			// reading)). For struct types (our most common case), the type's
			// function will do ratchet checks on each sub-field anyway.
			//
			// This leaves one case where validation is executed unilaterally:
			// non-pre-checked calls of validation functions for types which have
			// type-attached validations.  This can happen in two cases:
			//   1. an external caller of a type's validation function
			//   2. the generated register function for a package which calls a
			//      root-type's validation function
			//
			// TODO: We are leaving this as a problem for the future. If we
			// find that we have a root type which has type-attached validation
			// AND that validation is being ratcheted, then we will need to
			// address this. Some options:
			//   1. emit a ratchet check in the package's register function
			//   2. emit a ratchet check in the type's validation function
			//      (IFF it has type-attached validation, perhaps only for root
			//      types)
			//   3. emit both "safe" (ratchet check the whole object) and
			//      "fast" (assume the object was already ratchet checked)
			//      forms of each type's validation function, so that the
			//      generated code can call the "fast" form while external code
			//      calls the "safe" form.
			//   4. implement depth-first traversal of validation, where each
			//      function returns an additional bool indicating "something
			//      changed", which gets propagated up the caller to decide if
			//      it needs to do higher-level validations (e.g. if any field
			//      in a struct changes, the struct's type-attached validations
			//      need to be executed, but if no fields changed they can be
			//      skipped).

			validations := fld.fieldValidations
			fldRatchetingChecked := false
			if !validations.Empty() {
				emitComments(validations.Comments, bufsw)
				if len(validations.Functions) > 0 {
					emitRatchetingCheck(c, fld.childType, bufsw)
					fldRatchetingChecked = true
					bufsw.Do("// call field-attached validations\n", nil)
					emitCallsToValidators(c, validations.Functions, bufsw)
				}
			}

			// If the node is nil, this must be a type in a package we are not
			// handling - it's effectively opaque to us.
			if fld.node != nil {
				// Get to the real type.
				switch fld.node.valueType.Kind {
				case types.Alias, types.Struct:
					// If this field is another type, we may need to call its
					// validation function. If it has no validations
					// (transitively) then we don't need to do anything.
					if g.hasValidations(fld.node) {
						if !fldRatchetingChecked {
							emitRatchetingCheck(c, fld.childType, bufsw)
							fldRatchetingChecked = true
						}
						g.emitCallToOtherTypeFunc(c, fld.node, bufsw)
					}
				case types.Slice:
					// If this field is a list and the value-type has
					// validations, call its validation function.
					if validations := fld.fieldValIterations; g.hasValidations(fld.node.elem.node) && !validations.Empty() {
						emitComments(validations.Comments, bufsw)
						if len(validations.Functions) > 0 {
							if !fldRatchetingChecked {
								emitRatchetingCheck(c, fld.childType, bufsw)
								fldRatchetingChecked = true
							}
							emitCallsToValidators(c, validations.Functions, bufsw)
						}

					}
					// Descend into this field.
					g.emitValidationForChild(c, fld, bufsw)
				case types.Map:
					// If this field is a map and the key-type has
					// validations, call its validation function.
					if validations := fld.fieldKeyIterations; g.hasValidations(fld.node.key.node) && !validations.Empty() {
						emitComments(validations.Comments, bufsw)
						if len(validations.Functions) > 0 {
							if !fldRatchetingChecked {
								emitRatchetingCheck(c, fld.childType, bufsw)
								fldRatchetingChecked = true
							}
							emitCallsToValidators(c, validations.Functions, bufsw)
						}
					}
					// If this field is a map and the value-type has
					// validations, call its validation function.
					if validations := fld.fieldValIterations; g.hasValidations(fld.node.elem.node) && !validations.Empty() {
						emitComments(validations.Comments, bufsw)
						if len(validations.Functions) > 0 {
							if !fldRatchetingChecked {
								emitRatchetingCheck(c, fld.childType, bufsw)
								fldRatchetingChecked = true
							}
							emitCallsToValidators(c, validations.Functions, bufsw)
						}
					}
					// Descend into this field.
					g.emitValidationForChild(c, fld, bufsw)
				default:
					// Descend into this field.
					g.emitValidationForChild(c, fld, bufsw)
				}
			}

			if buf.Len() > 0 {
				leafType, typePfx, exprPfx := getLeafTypeAndPrefixes(fld.childType)
				targs := targs.WithArgs(generator.Args{
					"fieldName":    fld.name,
					"fieldJSON":    fld.jsonName,
					"fieldType":    leafType,
					"fieldTypePfx": typePfx,
					"fieldExprPfx": exprPfx,
				})

				if didSome {
					sw.Do("\n", nil)
				}
				sw.Do("// field $.inType|raw$.$.fieldName$\n", targs)
				sw.Do("errs = append(errs,\n", targs)
				sw.Do("  func(fldPath *$.field.Path|raw$, obj, oldObj $.fieldTypePfx$$.fieldType|raw$, oldValueCorrelated bool) (errs $.field.ErrorList|raw$) {\n", targs)
				if err := sw.Merge(buf, bufsw); err != nil {
					panic(fmt.Sprintf("failed to merge buffer: %v", err))
				}
				sw.Do("    return\n", targs)
				sw.Do("  }(", targs)
				if len(fld.jsonName) > 0 {
					sw.Do("fldPath.Child(\"$.fieldJSON$\"), ", targs)
				} else {
					// If there is an embedded field in a root-type, fldPath
					// will be nil, and we need SOMETHING for the field path.
					sw.Do("$.safe.Value|raw$(fldPath, func() *$.field.Path|raw$ { return fldPath.Child(\"$.fieldType|raw$\") }), ", targs)
				}
				sw.Do("    $.fieldExprPfx$obj.$.fieldName$, ", targs)
				// safe.Field returns a nil if the old object does not have a correlatable
				// value, such as a map.
				// This is ambiguous with the case where the field exists and is nil.
				// This ambiguity is a problem for ratcheting, which needs to distinguish
				// these cases. For example, if a required field is removed from a map,
				// safe.Field will return nil for the old value, and the new value is also
				// nil (because it doesn't exist). Ratcheting would normally allow this,
				// but it's a validation failure because a required field is missing.
				//
				// To solve this, we pass an extra boolean parameter to the validation
				// function, indicating whether the old value was correlated. If the old
				// value was uncorrelated, it means the field was not present in the old
				// object, and we should not apply ratcheting logic. `oldObj != nil`
				// provides this bit of information.
				//
				// This bit is not currently propagated down to deeper levels of
				// validation, but since the code generator only ever looks one level
				// down, this is sufficient for now.
				sw.Do("    $.safe.Field|raw$(oldObj, ", targs)
				sw.Do("        func(oldObj *$.inType|raw$) $.fieldTypePfx$$.fieldType|raw$ {", targs)
				sw.Do("            return $.fieldExprPfx$oldObj.$.fieldName$", targs)
				sw.Do("        }), oldObj != nil", targs)
				sw.Do("    )...)\n", targs)
				sw.Do("\n", nil)
			} else {
				targs := targs.WithArgs(generator.Args{
					"fieldName": fld.name,
				})
				sw.Do("// field $.inType|raw$.$.fieldName$ has no validation\n", targs)
			}
			didSome = true
		}
	default:
		panic(fmt.Sprintf("unhandled type: %v (kind %s)", inType, inType.Kind))
	}
}

// emitCallToOtherTypeFunc generates a call to the specified node's generated
// validation function for a field in some parent context.
//
// Emitted code assumes that the value in question is always a pair of nilable
// variables named "obj" and "oldObj", and the field path to this value is
// named "fldPath".
func (g *genValidations) emitCallToOtherTypeFunc(c *generator.Context, node *typeNode, sw *generator.SnippetWriter) {
	targs := generator.Args{
		"funcName": c.Universe.Type(node.funcName),
	}
	sw.Do("// call the type's validation function\n", nil)
	sw.Do("errs = append(errs, $.funcName|raw$(ctx, op, fldPath, obj, oldObj)...)\n", targs)
}

// emitRatchetingCheck emits an equivalence check for default ratcheting.
func emitRatchetingCheck(c *generator.Context, t *types.Type, sw *generator.SnippetWriter) {
	// Emit equivalence check for default ratcheting.
	targs := generator.Args{
		"operation": mkSymbolArgs(c, operationPkgSymbols),
	}
	sw.Do("// don't revalidate unchanged data\n", nil)
	// If the type is a builtin, we can use a simpler equality check when they are not nil.
	if util.IsDirectComparable(util.NonPointer(util.NativeType(t))) {
		// We should never get anything but pointers here, since every other
		// nilable type is not Comparable.
		//
		// This condition looks overly complex, but each case is needed:
		// - obj == oldObj : handle pointers which are nil in old and new
		// - obj != nil : handle optional fields which are updated to nil
		// - oldObj != nil : handle optional fields which are updated from nil
		// - *obj == *oldObj : compare values
		sw.Do("if oldValueCorrelated && op.Type == $.operation.Update|raw$ && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {\n", targs)
	} else {
		targs["equality"] = mkSymbolArgs(c, equalityPkgSymbols)
		sw.Do("if oldValueCorrelated && op.Type == $.operation.Update|raw$ && $.equality.Semantic|raw$.DeepEqual(obj, oldObj) {\n", targs)
	}
	sw.Do("   return nil\n", nil)
	sw.Do("}\n", nil)
}

// emitCallsToValidators emits calls to a list of validation functions for
// a single field or type. validations is a list of functions to call, with
// arguments.
//
// When calling registered validators, we always pass a nilable type.  E.g. if
// the field's type is string, we pass *string, and if the field's type is
// *string, we also pass *string.  This means that validators need to do
// nil-checks themselves, if they intend to dereference the pointer.  This
// makes updates more consistent.
//
// Emitted code assumes that the value in question is always a pair of nilable
// variables named "obj" and "oldObj", and the field path to this value is
// named "fldPath".
func emitCallsToValidators(c *generator.Context, validations []validators.FunctionGen, sw *generator.SnippetWriter) {
	// Group and sort the inputs.
	cohorts := sortIntoCohorts(validations)

	for _, validations := range cohorts {
		cohortName := validations[0].Cohort
		if cohortName != "" {
			sw.Do("func() { // cohort $.$\n", cohortName)
		}

		hasShortCircuits := false
		lastShortCircuitIdx := -1
		for i, v := range validations {
			if v.Flags.IsSet(validators.ShortCircuit) {
				hasShortCircuits = true
				lastShortCircuitIdx = i
			}
		}

		if hasShortCircuits {
			sw.Do("earlyReturn := false\n", nil)
		}

		for i, v := range validations {
			isShortCircuit := v.Flags.IsSet(validators.ShortCircuit)
			isNonError := v.Flags.IsSet(validators.NonError)

			targs := generator.Args{
				"funcName": c.Universe.Type(v.Function),
				"field":    mkSymbolArgs(c, fieldPkgSymbols),
			}

			emitCall := func() {
				sw.Do("$.funcName|raw$", targs)
				if typeArgs := v.TypeArgs; len(typeArgs) > 0 {
					sw.Do("[", nil)
					for i, typeArg := range typeArgs {
						sw.Do("$.|raw$", c.Universe.Type(typeArg))
						if i < len(typeArgs)-1 {
							sw.Do(",", nil)
						}
					}
					sw.Do("]", nil)
				}
				sw.Do("(ctx, op, fldPath, obj, oldObj", targs)
				for _, arg := range v.Args {
					sw.Do(", ", nil)
					toGolangSourceDataLiteral(sw, c, arg)
				}
				sw.Do(")", targs)
			}

			// If validation is conditional, wrap the validation function with a conditions check.
			if !v.Conditions.Empty() {
				emitBaseFunction := emitCall
				emitCall = func() {
					sw.Do("func() $.field.ErrorList|raw$ {\n", targs)
					sw.Do("  if ", nil)
					firstCondition := true
					if len(v.Conditions.OptionEnabled) > 0 {
						sw.Do("op.HasOption($.$)", strconv.Quote(v.Conditions.OptionEnabled))
						firstCondition = false
					}
					if len(v.Conditions.OptionDisabled) > 0 {
						if !firstCondition {
							sw.Do(" && ", nil)
						}
						sw.Do("!op.HasOption($.$)", strconv.Quote(v.Conditions.OptionDisabled))
					}
					sw.Do(" {\n", nil)
					sw.Do("    return ", nil)
					emitBaseFunction()
					sw.Do("\n", nil)
					sw.Do("  } else {\n", nil)
					sw.Do("    return nil // skip validation\n", nil)
					sw.Do("  }\n", nil)
					sw.Do("}()", nil)
				}
			}

			for _, comment := range v.Comments {
				sw.Do("// $.$\n", comment)
			}
			if isShortCircuit {
				sw.Do("if e := ", nil)
				emitCall()
				sw.Do("; len(e) != 0 {\n", nil)
				if !isNonError {
					sw.Do("  errs = append(errs, e...)\n", nil)
				}
				sw.Do("  earlyReturn = true\n", nil)
				sw.Do("}\n", nil)

				// Check for early return ONLY after the LAST short-circuit
				if hasShortCircuits && i == lastShortCircuitIdx {
					sw.Do("if earlyReturn {\n", nil)
					sw.Do("  return // do not proceed\n", nil)
					sw.Do("}\n", nil)
				}
			} else {
				if isNonError {
					emitCall()
				} else {
					sw.Do("errs = append(errs, ", nil)
					emitCall()
					sw.Do("...)\n", nil)
				}
			}
		}
		if cohortName != "" {
			sw.Do("}()\n", nil)
		}
	}
}

// sortIntoCohorts groups the inputs into a list of cohorts. Within each
// cohort, function calls are sorted such that short-circuiting function
// calls are handled before others. The first cohort is always the
// default cohort (named "") if it exists. Other cohorts are returned in
// the order they were defined in the input.
func sortIntoCohorts(in []validators.FunctionGen) [][]validators.FunctionGen {
	defaultCohort := make([]validators.FunctionGen, 0, len(in))
	namedCohorts := map[string][]validators.FunctionGen{}
	idx := make([]string, 0, len(in))
	for _, fg := range in {
		key := fg.Cohort
		if key == "" {
			defaultCohort = append(defaultCohort, fg)
		} else {
			if !slices.Contains(idx, key) {
				idx = append(idx, key)
			}
			namedCohorts[key] = append(namedCohorts[key], fg)
		}
	}
	if len(defaultCohort) > 0 {
		idx = append([]string{""}, idx...)
	}
	// NOTE: we do not sort cohorts by name, because we want to preserve
	// their definition order.

	result := make([][]validators.FunctionGen, 0, len(in))
	for _, key := range idx {
		var cohort []validators.FunctionGen
		if key == "" {
			cohort = defaultCohort
		} else {
			cohort = namedCohorts[key]
		}

		sooner := make([]validators.FunctionGen, 0, len(cohort))
		later := make([]validators.FunctionGen, 0, len(cohort))

		for _, fg := range cohort {
			isShortCircuit := (fg.Flags.IsSet(validators.ShortCircuit))

			if isShortCircuit {
				sooner = append(sooner, fg)
			} else {
				later = append(later, fg)
			}
		}
		sorted := sooner
		sorted = append(sorted, later...)
		result = append(result, sorted)
	}
	return result
}

func emitComments(comments []string, sw *generator.SnippetWriter) {
	for _, comment := range comments {
		sw.Do("// ", nil)
		sw.Do(comment, nil)
		sw.Do("\n", nil)
	}
}

// emitValidationVariables emits a list of variable declarations. Each variable declaration has a
// private (unexported) variable name, and a function invocation declaration that is expected
// to initialize the value of the variable.
func (g *genValidations) emitValidationVariables(c *generator.Context, t *types.Type, sw *generator.SnippetWriter) {
	tn := g.discovered.typeNodes[t]

	emit := func(variables []validators.VariableGen) {
		slices.SortFunc(variables, func(a, b validators.VariableGen) int {
			return cmp.Compare(a.Variable.Name, b.Variable.Name)
		})
		for _, variable := range variables {
			targs := generator.Args{
				"varName": c.Universe.Type(types.Name(variable.Variable)),
			}

			sw.Do("var $.varName|private$ = ", targs)
			toGolangSourceDataLiteral(sw, c, variable.Initializer)
			sw.Do("\n", nil)
		}
	}
	// TODO: Handle potential variable name collisions when multiple validators
	// generate variables with the same name.
	emit(tn.typeValidations.Variables)
	for _, field := range tn.fields {
		if len(field.fieldValidations.Variables) != 0 {
			emit(field.fieldValidations.Variables)
		}
	}
}

func toGolangSourceDataLiteral(sw *generator.SnippetWriter, c *generator.Context, value any) {
	// For safety, be strict in what values we output to visited source, and ensure strings
	// are quoted.

	switch v := value.(type) {
	case uint, uint8, uint16, uint32, uint64, int, int8, int16, int32, int64, float32, float64, bool:
		sw.Do(fmt.Sprintf("%v", value), nil)
	case string:
		// If the incoming string was quoted, we still do it ourselves, JIC.
		str := value.(string)
		if s, err := strconv.Unquote(str); err == nil {
			str = s
		}
		sw.Do(fmt.Sprintf("%q", str), nil)
	case *types.Type:
		sw.Do("$.|raw$", v)
	case types.Member:
		sw.Do("obj."+v.Name, nil)
	case *types.Member:
		sw.Do("obj."+v.Name, nil)
	case validators.Identifier:
		sw.Do("$.|raw$", c.Universe.Type(types.Name(v)))
	case *validators.Identifier:
		sw.Do("$.|raw$", c.Universe.Type(types.Name(*v)))
	case validators.PrivateVar:
		sw.Do("$.|private$", c.Universe.Type(types.Name(v)))
	case *validators.PrivateVar:
		sw.Do("$.|private$", c.Universe.Type(types.Name(*v)))
	case validators.WrapperFunction:
		if extraArgs := v.Function.Args; len(extraArgs) == 0 {
			// If the function to be wrapped has no additional arguments, we can
			// just use it directly.
			targs := generator.Args{
				"funcName": c.Universe.Type(v.Function.Function),
			}
			for _, comment := range v.Function.Comments {
				sw.Do("// $.$\n", comment)
			}
			sw.Do("$.funcName|raw$", targs)
		} else {
			// If the function to be wrapped has additional arguments, we need
			// a "standard signature" validation function to wrap it.
			targs := generator.Args{
				"funcName":   c.Universe.Type(v.Function.Function),
				"field":      mkSymbolArgs(c, fieldPkgSymbols),
				"operation":  mkSymbolArgs(c, operationPkgSymbols),
				"context":    mkSymbolArgs(c, contextPkgSymbols),
				"objType":    v.ObjType,
				"objTypePfx": "*",
			}
			if util.IsNilableType(v.ObjType) {
				targs["objTypePfx"] = ""
			}

			sw.Do("func(", targs)
			sw.Do("    ctx $.context.Context|raw$, ", targs)
			sw.Do("    op $.operation.Operation|raw$, ", targs)
			sw.Do("    fldPath *$.field.Path|raw$, ", targs)
			sw.Do("    obj, oldObj $.objTypePfx$$.objType|raw$ ", targs)
			sw.Do(")    $.field.ErrorList|raw$ {\n", targs)
			sw.Do("return ", nil)
			emitFunctionCall(sw, c, v.Function, "ctx", "op", "fldPath", "obj", "oldObj")
			sw.Do("\n}", targs)
		}
	case validators.Literal:
		sw.Do("$.$", v)
	case validators.FunctionGen:
		for _, comment := range v.Comments {
			sw.Do("// $.$\\n", comment)
		}
		emitFunctionCall(sw, c, v)
	case validators.FunctionLiteral:
		sw.Do("func(", nil)
		for i, param := range v.Parameters {
			if i > 0 {
				sw.Do(", ", nil)
			}
			targs := generator.Args{
				"name": param.Name,
				"type": param.Type,
			}
			sw.Do("$.name$ $.type|raw$", targs)
		}
		sw.Do(")", nil)
		if len(v.Results) > 1 {
			sw.Do(" (", nil)
		}
		for i, ret := range v.Results {
			if i > 0 {
				sw.Do(", ", nil)
			}
			targs := generator.Args{
				"name": ret.Name,
				"type": ret.Type,
			}
			sw.Do("$.name$ $.type|raw$", targs)
		}
		if len(v.Results) > 1 {
			sw.Do(")", nil)
		}
		sw.Do(" { $.$ }", v.Body)
	case validators.StructLiteral:
		targs := generator.Args{
			"type": c.Universe.Type(v.Type),
		}
		sw.Do("$.type|raw$", targs)
		if len(v.TypeArgs) > 0 {
			sw.Do("[", nil)
			for i, typeArg := range v.TypeArgs {
				if i > 0 {
					sw.Do(", ", nil)
				}
				sw.Do("$.|raw$", typeArg)
			}
			sw.Do("]", nil)
		}
		sw.Do("{\n", nil)
		for _, f := range v.Fields {
			sw.Do(f.Name, nil)
			sw.Do(": ", nil)
			toGolangSourceDataLiteral(sw, c, f.Value)
			sw.Do(", ", nil)
		}
		sw.Do("}", targs)
	case validators.SliceLiteral:
		sw.Do("[]", nil)
		targs := generator.Args{
			"type": c.Universe.Type(v.ElementType),
		}
		sw.Do("$.type|raw$", targs)
		if len(v.ElementTypeArgs) > 0 {
			sw.Do("[", nil)
			for i, typeArg := range v.ElementTypeArgs {
				if i > 0 {
					sw.Do(", ", nil)
				}
				sw.Do("$.|raw$", typeArg)
			}
			sw.Do("]", nil)
		}
		sw.Do("{\n", nil)
		for _, e := range v.Elements {
			toGolangSourceDataLiteral(sw, c, e)
			sw.Do(",\n", nil)
		}
		sw.Do("}", nil)
	default:
		rv := reflect.ValueOf(value)
		switch rv.Kind() {
		case reflect.Array:
			arraySize := ""
			if rv.Kind() == reflect.Array {
				arraySize = strconv.Itoa(rv.Len())
			}
			var itemType string
			switch rv.Type().Elem().Kind() {
			case reflect.String: // For now, only support lists of strings.
				itemType = rv.Type().Elem().Name()
			default:
				panic(fmt.Sprintf("Unsupported extraArg type: %T", value))
			}
			rv.Type().Elem()
			sw.Do("[$.arraySize$]$.itemType${", map[string]string{"arraySize": arraySize, "itemType": itemType})
			for i := range rv.Len() {
				val := rv.Index(i)
				toGolangSourceDataLiteral(sw, c, val.Interface())
				if i < rv.Len()-1 {
					sw.Do(", ", nil)
				}
			}
			sw.Do("}", nil)
		default:
			// TODO: check this during discovery and emit an error with more useful information
			panic(fmt.Sprintf("Unsupported extraArg type: %T", value))
		}
	}
}

func emitFunctionCall(sw *generator.SnippetWriter, c *generator.Context, v validators.FunctionGen, leadingArgs ...string) {
	targs := generator.Args{
		"funcName": c.Universe.Type(v.Function),
	}
	sw.Do("$.funcName|raw$", targs)
	if typeArgs := v.TypeArgs; len(typeArgs) > 0 {
		sw.Do("[", nil)
		for i, typeArg := range typeArgs {
			sw.Do("$.|raw$", c.Universe.Type(typeArg))
			if i < len(typeArgs)-1 {
				sw.Do(",", nil)
			}
		}
		sw.Do("]", nil)
	}
	sw.Do("(", nil)
	if len(leadingArgs) > 0 {
		sw.Do(strings.Join(leadingArgs, ", "), nil)
	}
	if len(leadingArgs) > 0 && len(v.Args) > 0 {
		sw.Do(", ", nil)
	}
	for i, arg := range v.Args {
		if i != 0 {
			sw.Do(", ", nil)
		}
		toGolangSourceDataLiteral(sw, c, arg)
	}
	sw.Do(")", nil)
}

// getLeafTypeAndPrefixes returns the "leaf value type" for a given type, as
// well as type and expression prefix strings for the input type.  The type
// prefix can be prepended to the given type's name to produce the nilable form
// of that type.  The expression prefix can be prepended to a variable of the
// given type to produce the nilable form of that value.
//
// Example: Given an input type "string" this should produce (string, "*", "&").
// That is to say: the value-type is "string", which yields "*string" when the
// type prefix is applied, and a variable "x" becomes "&x" when the expression
// prefix is applied.
//
// Example: Given an input type "*string" this should produce (string, "*", "").
// That is to say: the value-type is "string", which yields "*string" when the
// type prefix is applied, and a variable "x" remains "x" when the expression
// prefix is applied.
func getLeafTypeAndPrefixes(inType *types.Type) (*types.Type, string, string) {
	leafType := inType
	typePfx := ""
	exprPfx := ""

	nPtrs := 0
	for leafType.Kind == types.Pointer {
		nPtrs++
		leafType = leafType.Elem
	}
	if !util.IsNilableType(leafType) {
		typePfx = "*"
		if nPtrs == 0 {
			exprPfx = "&"
		} else {
			exprPfx = strings.Repeat("*", nPtrs-1)
		}
	} else {
		exprPfx = strings.Repeat("*", nPtrs)
	}

	return leafType, typePfx, exprPfx
}

// FixtureTests generates a test file that checks all validateFalse validations.
func FixtureTests(outputFilename string, testFixtureTags sets.Set[string]) generator.Generator {
	return &fixtureTestGen{
		GoGenerator: generator.GoGenerator{
			OutputFilename: outputFilename,
		},
		testFixtureTags: testFixtureTags,
	}
}

type fixtureTestGen struct {
	generator.GoGenerator
	testFixtureTags sets.Set[string]
}

func (g *fixtureTestGen) Imports(_ *generator.Context) (imports []string) {
	return []string{`"testing"`}
}

func (g *fixtureTestGen) Init(c *generator.Context, w io.Writer) error {
	if g.testFixtureTags.Has("validateFalse") {
		sw := generator.NewSnippetWriter(w, c, "$", "$")
		sw.Do("func TestValidation(t *testing.T) {\n", nil)
		sw.Do("  localSchemeBuilder.Test(t).ValidateFixtures()\n", nil)
		sw.Do("}\n", nil)
	}
	return nil
}
